﻿using System;
using System.Configuration;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Security;
using LeastPrivilege.CustomBasicAuthentication.Management;
using Microsoft.Web.Administration;

namespace LeastPrivilege.CustomBasicAuthentication
{
	public partial class UnregisteredAuthenticationModule
	{
		private UnregisteredAuthenticationSection configuration = null;
		private string cachePrefix = "_lp_CustomBasicAuth_";

		#region Properties
		protected UnregisteredAuthenticationSection Configuration
		{
			get
			{
				if (configuration == null)
				{
					configuration = (UnregisteredAuthenticationSection)System.Configuration.ConfigurationManager.GetSection(UnregisteredAuthenticationSection.SectionName);
				}
				// TODO: Error handling
				return configuration;
			}
		}

		protected bool IsAnonymousAllowed
		{
			get
			{
				return false;
			}
		}

		protected MembershipProvider Provider
		{
			get
			{
				if (string.Equals(Configuration.ProviderName, "default", StringComparison.OrdinalIgnoreCase))
					return Membership.Provider;
				else
					return Membership.Providers[Configuration.ProviderName];
			}
		}

		protected bool IsHeaderPresent
		{
			get
			{
				HttpContext context = HttpContext.Current;
				string authHeader = context.Request.Headers["Authorization"];
				return (!string.IsNullOrEmpty(authHeader));
			}
		}
		#endregion
		private bool AuthenticateUser()
		{
			string username = "", password = "";
			string authHeader = HttpContext.Current.Request.Headers["Authorization"];

			if (authHeader != null && authHeader.StartsWith("Basic"))
			{
				// Extract credentials from header
				string[] credentials = ExtractCredentials(authHeader);
				username = credentials[0];
				password = credentials[1];

				if (IsCredentialCached(username, password))
				{
					SetPrincipal(username);
					return true;
				}
				else if (Provider.ValidateUser(username, password))
				{
					if (Configuration.CachingEnabled)
					{
						CacheCredential(username, password);
					}
					SetPrincipal(username);
					new AuthenticationSuccessEvent(this, username).Raise();
					return true;
				}
			}

			new AuthenticationFailureEvent(this, username).Raise();
			return false;
		}

		// Set a 401 and end the current request processing will jump to EndRequest
		private static void DenyAccess()
		{
			HttpContext context = HttpContext.Current;
			context.Response.StatusCode = 401;
			context.Response.End();
		}

		// Caches a credential identifier
		private void CacheCredential(string username, string password)
		{
			string identifier = GetIdentifier(username, password);

			try
			{
				HttpContext.Current.Cache.Add(
					identifier,
					"ok",
					null,
					DateTime.Now.AddMinutes(Configuration.CachingDuration),
					Cache.NoSlidingExpiration,
					CacheItemPriority.Normal,
					null);
				new CredentialCacheAddEvent(this, username).Raise();
			}
			catch (Exception ex)
			{
				new CredentialCacheAddErrorEvent(this, username, ex).Raise();
			}
		}

		// Checks if the credential has been cached already
		private bool IsCredentialCached(string username, string password)
		{
			if (!Configuration.CachingEnabled)
				return false;

			string identifier = GetIdentifier(username, password);
			bool cacheHit = (HttpContext.Current.Cache[identifier] != null);

			if (cacheHit)
			{
				new CredentialCacheHitEvent(this, username).Raise();
			}
			else
			{
				new CredentialCacheMissEvent(this, username).Raise();
			}

			return cacheHit;
		}

		// Create GenericPrincipal and set it on Context.User
		private static void SetPrincipal(string username)
		{
			//Create Principal and set Context.User
			GenericIdentity id = new GenericIdentity(username, "CustomBasic");
			GenericPrincipal p = new GenericPrincipal(id, null);
			HttpContext.Current.User = p;
		}

		// Send header to start Basic Authentication handshake
		private void SendAuthenticationHeader()
		{
			HttpContext context = HttpContext.Current;

			context.Response.StatusCode = 401;
			context.Response.AddHeader("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", Configuration.Realm));
		}

		// Extracts and decodes username and password from the auth header
		private string[] ExtractCredentials(string authHeader)
		{
			// Strip out the "basic"
			string encodedUserPass = authHeader.Substring(6).Trim();

			// That's the right encoding
			Encoding encoding = Encoding.GetEncoding("iso-8859-1");
			string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
			int separator = userPass.IndexOf(':');

			string[] credentials = new string[2];
			credentials[0] = userPass.Substring(0, separator);
			credentials[1] = userPass.Substring(separator + 1);

			return credentials;
		}

		/// <summary>
		/// Create a string identifier for the cache key
		/// Format: prefix + Base64(Hash(username + password))
		/// </summary>
		/// <param name="username"></param>
		/// <param name="password"></param>
		/// <returns></returns>
		private string GetIdentifier(string username, string password)
		{
			// Use default hash algorithm configured on this machine via CryptoMappings
			// Usually SHA1CryptoServiceProvider
			HashAlgorithm hash = HashAlgorithm.Create();

			string identifier = username + password;
			byte[] identifierBytes = Encoding.UTF8.GetBytes(identifier);
			byte[] identifierHash = hash.ComputeHash(identifierBytes);

			return cachePrefix + Convert.ToBase64String(identifierHash);
		}
		#region Helper

		#endregion
	}
}
